package com.gitee.qdbp.general.system.web.shiro.simple;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.subject.WebSubject;
import org.apache.shiro.web.subject.support.WebDelegatingSubject;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.base.shiro.realm.ManagableLoginRealm;
import com.gitee.qdbp.base.shiro.token.AuthedLoginToken;
import com.gitee.qdbp.base.shiro.token.FormParamToken;
import com.gitee.qdbp.base.system.model.IAccount;

/**
 * 用户登录基础处理类<br>
 * 登录不仅有账号密码登录, 还有短信验证码登录, 第三方登录<br>
 * 账号密码之外的其他登录逻辑通过AuthedLoginToken实现<br>
 * 验证逻辑通过setCredentialsMatcher()设置, 密码其实已经在登录环节验证过了, 改成AllowAllCredentialsMatcher
 *
 * @author zhaohuihua
 * @version 160816
 */
public abstract class SimpleAbstractLoginRealm extends ManagableLoginRealm {

    private static final Logger log = LoggerFactory.getLogger(SimpleAbstractLoginRealm.class);

    private boolean ensureLogoutBeforeLogin;
    private boolean clearAuthorizationInfo;
    private List<Object> clearAttributeKeepKeys = new ArrayList<>();
    @Autowired(required = false)
    private OnAuthEventHandler onLoginSuccessHandler;

    public SimpleAbstractLoginRealm() {
        super();
        // 密码已经校验过了
        this.setCredentialsMatcher(new AllowAllCredentialsMatcher());
        this.ensureLogoutBeforeLogin = true;
        this.clearAuthorizationInfo = true;
        this.addClearAttributeKeepKeys(WebUtils.SAVED_REQUEST_KEY, "OperateRecordGroup");
    }

    /** 验证当前登录的Subject **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
            throws AuthenticationException {

        // 执行登录前, 先让当前SESSION失效
        if (ensureLogoutBeforeLogin) {
            ensureSessionLogout();
        }

        IAccount user;
        if (authcToken instanceof AuthedLoginToken) {
            try {
                user = ((AuthedLoginToken) authcToken).getAccount();
            } catch (ServiceException e) {
                throw new AuthenticationException("Failed to exec authentication login.", e);
            }
        } else {
            try {
                user = doGetAuthenticationInfo((FormParamToken) authcToken);
            } catch (ServiceException e) {
                throw new AuthenticationException("Failed to exec authentication login.", e);
            }
        }

        // 密码在登录逻辑中已经验证过了
        // UsernamePasswordToken upt = (UsernamePasswordToken) authcToken;
        // String md5 = passwordService.create(user.getId(), new String(upt.getPassword()));
        // 将用户存放到登录认证info中
        // SimpleAuthenticationInfo sai = new SimpleAuthenticationInfo(user, md5, getName());
        // String salt = PasswordShiro.salt(user.getId());
        // sai.setCredentialsSalt(ByteSource.Util.bytes(salt));

        SimpleAuthenticationInfo sai = new SimpleAuthenticationInfo(user, null, getName());
        // 清除授权信息(新用户可能在redis之类的缓存中已存在授权信息)
        if (clearAuthorizationInfo) {
            this.clearCachedAuthorizationInfo(sai.getPrincipals());
        }

        log.trace("Successful to login: {}", user);

        try {
            onAuthenticatedSuccess(user);
        } catch (ServiceException e) {
            throw new AuthenticationException("Failed to call onAuthenticatedSuccess.", e);
        }

        return sai;
    }

    protected abstract IAccount doGetAuthenticationInfo(FormParamToken token) throws ServiceException;

    /** 登录成功之后的处理 **/
    protected final void onAuthenticatedSuccess(IAccount user) throws ServiceException {
        if (onLoginSuccessHandler != null) {
            onLoginSuccessHandler.onAuthenticatedSuccess(user);
        }
    }

    /** 授权成功之后的处理 **/
    protected final void onAuthorizeSuccess(IAccount user) throws ServiceException {
        if (onLoginSuccessHandler != null) {
            onLoginSuccessHandler.onAuthorizeSuccess(user);
        }
    }

    protected void ensureSessionLogout() {
        if (!ensureLogoutBeforeLogin) {
            return;
        }
        try {
            Subject subject = SecurityUtils.getSubject();
            if (subject == null) return;
            if (subject.getPrincipal() == null) return;

            Session session = subject.getSession(false);
            if (session == null) return;

            SecurityManager securityManager = SecurityUtils.getSecurityManager();
            String host = session.getHost();
            WebSubject webSubject = ((WebSubject) subject);
            ServletRequest req = webSubject.getServletRequest();
            ServletResponse resp = webSubject.getServletResponse();

            // 清除原用户的授权信息
            if (clearAuthorizationInfo) {
                this.clearCachedAuthorizationInfo(subject.getPrincipals());
            }

            List<Object> keys = new ArrayList<>();
            { // 复制AttributeKeys, 因为session.removeAttribute会导致Collection结构变化
                Collection<Object> temp = session.getAttributeKeys();
                for (Object o : temp) {
                    keys.add(o);
                }
            }

            // 清除SESSION中所有的attribute
            List<Object> removed = new ArrayList<>();
            for (Object key : keys) {
                if (clearAttributeKeepKeys.contains(key)) continue;
                session.removeAttribute(key);
                removed.add(key);
            }
            log.debug("Success to clear session attribute: {}", removed.toString());

            // 重新生成Subject
            // buildWebSubject会执行整个创建流程, 会从rememberMe中恢复session, 因此不能用
            // Subject newSubject = new WebSubject.Builder(securityManager, request, response).buildWebSubject();
            Subject newSubject = new WebDelegatingSubject(null, false, host, session, req, resp, securityManager);
            ThreadContext.bind(newSubject);
            log.debug("Success to rebuild subject.");
        } catch (Exception e) {
            // Ignore all errors, as we're trying to silently log the user out.
            log.warn("Failed to clear session attribute.", e);
        }
    }

    public boolean isEnsureLogoutBeforeLogin() {
        return ensureLogoutBeforeLogin;
    }

    public void setEnsureLogoutBeforeLogin(boolean ensureLogoutBeforeLogin) {
        this.ensureLogoutBeforeLogin = ensureLogoutBeforeLogin;
    }

    public boolean isClearAuthorizationInfo() {
        return clearAuthorizationInfo;
    }

    public void setClearAuthorizationInfo(boolean clearAuthorizationInfo) {
        this.clearAuthorizationInfo = clearAuthorizationInfo;
    }

    public List<Object> getClearAttributeKeepKeys() {
        return clearAttributeKeepKeys;
    }

    public void setClearAttributeKeepKeys(List<Object> clearAttributeKeepKeys) {
        this.clearAttributeKeepKeys = clearAttributeKeepKeys;
    }

    public void addClearAttributeKeepKeys(Object... keys) {
        for (Object key : keys) {
            this.clearAttributeKeepKeys.add(key);
        }
    }

    public OnAuthEventHandler getOnLoginSuccessHandler() {
        return onLoginSuccessHandler;
    }

    public void setOnLoginSuccessHandler(OnAuthEventHandler onLoginSuccessHandler) {
        this.onLoginSuccessHandler = onLoginSuccessHandler;
    }

}
